
Imports System.Threading
Imports System.IO
Imports std

Public Module deploys

    Private Class procInfo
        Public name As String = Nothing
        Public app As String = Nothing
        Public argument As String = Nothing
        Public ver As String = Nothing
        Public priority As String = Nothing
        Public proc As Process = Nothing
        Public apppath As String = Nothing
        Public appsrc As String = Nothing
    End Class

    Public Const defaultConfigfile As String = "deploys.ini"
    Private WithEvents config As configMonitor = Nothing
    Private WithEvents funcConfig As configMonitor = Nothing
    Private WithEvents tcpServer As tcpServer2 = Nothing
    Private machinefunction As vector(Of String) = Nothing
    Private apps As list(Of procInfo) = Nothing
    Private deployMutex As Mutex = Nothing

    Private Function functionlistFile() As String
        Return Path.Combine(sourcepath(), config.parameter(configNaming.deploys.name _
                                                            , configNaming.deploys.functionlist _
                                                            , configNaming.deploys.defaultValue.functionlist))
    End Function

    Private Function sourcepath() As String
        Return config.parameter(configNaming.deploys.name, configNaming.deploys.source _
                                , configNaming.deploys.defaultValue.source)
    End Function

    Private Function destpath() As String
        Return config.parameter(configNaming.deploys.name, configNaming.deploys.dest _
                                , configNaming.deploys.defaultValue.dest)
    End Function

    Private Function removebefore() As Boolean
        Return config.parameter(configNaming.deploys.name, configNaming.deploys.removebefore, _
                                configNaming.deploys.defaultValue.removebefore)
    End Function

    Private Sub sleepForQueue()
        Dim index As UInt64
        index = config.parameter(configNaming.deploys.name, configNaming.deploys.machineindex _
                                , configNaming.deploys.defaultValue.machineindex)
        Dim waittime As Int32
        waittime = config.parameter(configNaming.deploys.name, configNaming.deploys.waittime _
                                    , configNaming.deploys.defaultValue.waittime)
        sleep(waittime * index)
    End Sub

    Private Sub closeDeployApps()
        On Error Resume Next

        If Not apps Is Nothing Then
            Dim i As Int32
            For i = 0 To apps.size() - 1
                If Not apps(i) Is Nothing AndAlso Not apps(i).proc Is Nothing AndAlso Not apps(i).proc.HasExited Then
                    closeProcess(apps(i).proc)
                End If
            Next
            apps.clear()
        End If
    End Sub

    Private Function findapp(ByRef name As String) As procInfo
        Dim rtn As procInfo = Nothing
        Dim i As Int64
        For i = 0 To apps.size() - 1
            If Not apps(i) Is Nothing AndAlso strsame(apps(i).name, name) Then
                rtn = apps(i)
                Exit For
            End If
        Next
        Return rtn
    End Function

    Private Sub startDeploy()
        On Error Resume Next

        assert(deployMutex.WaitOne(), "cannot lock deployMutex when deploy.")

        If funcConfig Is Nothing Then
            funcConfig = New configMonitor
        Else
            funcConfig.close()
        End If
        funcConfig.open(functionlistFile())

        If apps Is Nothing Then
            apps = New list(Of procInfo)
        End If

        If Not Directory.Exists(destpath()) AndAlso Not createDirectory(destpath(), maxInt32) Then
            raiseError("cannot create dest directory " + destpath() + ", deployment failed." _
                        , errorHandle.errorType.exclamation)
            Exit Sub
        End If

        sleepForQueue()

        Dim functions As String = Nothing
        If Not funcConfig.getparameter(configNaming.machinefunction.name _
                                        , Environment.MachineName.ToLower, functions) _
            AndAlso Not funcConfig.getparameter(configNaming.machinefunction.name _
                                                , Environment.MachineName.ToUpper, functions) Then
            functions = funcConfig.parameter(configNaming.machinefunction.name _
                                            , configNaming.machinefunction.default _
                                            , configNaming.machinefunction.defaultValue.default)
        End If
        strsepar(functions, character.comma, machinefunction, True)

        Dim it As list(Of procInfo).iterator = Nothing
        Dim i As Int32
        it = apps.begin()
        While it <> apps.end()
            For i = 0 To machinefunction.size() - 1
                If strsame((+it).name, machinefunction(i)) Then
                    Exit For
                End If
            Next
            If i = machinefunction.size() Then
                raiseError("remove machinefunction " + (+it).name, errorHandle.errorType.application)
                If Not (+it).proc Is Nothing Then
                    closeProcess((+it).proc)
                End If
                apps.erase(it)
            End If
            it += 1
        End While

        For i = 0 To machinefunction.size() - 1
            If Not isemptystring(machinefunction(i)) AndAlso funcConfig.existSection(machinefunction(i)) Then
                Dim app As String = Nothing
                app = funcConfig.parameter(Of String)(machinefunction(i), configNaming.functionlist.app)
                Dim argument As String = Nothing
                argument = funcConfig.parameter(Of String)(machinefunction(i), configNaming.functionlist.argument)
                Dim ver As String = Nothing
                ver = funcConfig.parameter(machinefunction(i), configNaming.functionlist.ver _
                                           , configNaming.functionlist.defaultValue.ver)
                Dim priority As String = Nothing
                priority = funcConfig.parameter(machinefunction(i), configNaming.functionlist.priority _
                                                , configNaming.functionlist.defaultValue.priority)
                Dim apppath As String = Nothing
                apppath = funcConfig.parameter(Of String)(machinefunction(i), configNaming.functionlist.apppath _
                                                          , machinefunction(i))
                Dim appsrc As String = Nothing
                appsrc = funcConfig.parameter(Of String)(machinefunction(i) _
                                                          , configNaming.functionlist.appsrc, apppath)
                Dim procInfo As procInfo = Nothing
                procInfo = findapp(machinefunction(i))
                If procInfo Is Nothing OrElse _
                   Not strsame(procInfo.app, app, False) OrElse _
                   Not strsame(procInfo.argument, argument, False) OrElse _
                   Not strsame(procInfo.ver, ver, False) OrElse _
                   Not strsame(procInfo.priority, priority, False) OrElse _
                   Not strsame(procInfo.apppath, apppath, False) OrElse _
                   Not strsame(procInfo.appsrc, appsrc, False) Then
                    ' deploy this application
                    Dim found As Boolean = False
                    If Not procInfo Is Nothing Then
                        raiseError("caught machinefunction " + machinefunction(i) + " changed, deploy again." _
                                    , errorHandle.errorType.application)
                        If Not procInfo.proc Is Nothing Then
                            closeProcess(procInfo.proc, , False)
                        End If
                        found = True
                    Else
                        raiseError("found new machinefunction " + machinefunction(i) + ", deploy." _
                                    , errorHandle.errorType.application)
                        procInfo = New procInfo
                        copy(procInfo.name, machinefunction(i))
                    End If
                    Dim appsourcepath As String = Nothing
                    appsourcepath = Path.Combine(sourcepath(), appsrc)
                    Dim appdestpath As String = Nothing
                    appdestpath = Path.Combine(destpath(), apppath)

                    'already move all data / log to other folders, so it's safe to delete original app now
                    'do not break deployment if cannot remove original app
                    If removebefore() AndAlso Not deleteDirectory(appdestpath) Then
                        raiseError("cannot remove original app " + machinefunction(i) + _
                                       " in " + appdestpath, errorHandle.errorType.exclamation)
                    End If
                    If Not copyDirectory(appsourcepath, appdestpath, False, maxInt32, True) Then
                        raiseError("cannot copy app " + machinefunction(i) + _
                                   " from " + appsourcepath + " to " + appdestpath + ", ignore.", _
                                   errorHandle.errorType.exclamation)
                    Else
                        If Not isemptystring(app) Then
                            raiseError("start app " + app + ", with argument " + argument)
                            Dim proc As Process = Nothing
                            proc = New Process()
                            proc.StartInfo.FileName = Path.Combine(fullpath(appdestpath), app)
                            proc.StartInfo.WorkingDirectory = appdestpath
                            proc.StartInfo.Arguments = argument
                            If Not proc.Start Then
                                raiseError("cannot start process " + app + ", with argument " + argument _
                                            , errorHandle.errorType.exclamation)
                            Else
#If Not PocketPC AndAlso Not Smartphone Then
                                proc.PriorityClass = processPriority(priority)
#End If
                                procInfo.proc = proc
                            End If
                        End If
                        copy(procInfo.app, app)
                        copy(procInfo.argument, argument)
                        copy(procInfo.ver, ver)
                        copy(procInfo.priority, priority)
                        copy(procInfo.apppath, apppath)
                        copy(procInfo.appsrc, appsrc)
                        If Not found Then
                            apps.push_back(procInfo)
                        End If
                    End If
                End If
            End If
        Next

        deployMutex.ReleaseMutex()
    End Sub

    Private Sub startService()
        registerConsoleExtHandle()
        startDeploy()
        tcpServer = New tcpServer2()
        If Not tcpServer.start(config.parameter(configNaming.deploys.name, _
                                                configNaming.deploys.tcpport, _
                                                configNaming.deploys.defaultValue.tcpport)) Then
            raiseError("cannot start tcp server to handle remote commands, ignore.", errorHandle.errorType.system)
        End If

        While Not consoleExtSignal()
            sleep()
        End While
        raiseError("caught console cancel key event, start closeDeployApps.")

        tcpServer.stop()
        closeDeployApps()
        consoleExtHandleFinish()
    End Sub

    Public Sub unhandldedException(ByVal ex As Exception)
        closeDeployApps()
        finishmain()
    End Sub

    Public Sub main(ByVal args() As String)
        errorHandle.initial(defaultLogFilename())
        destructor.start()
        registerUnhandledException(AddressOf unhandldedException)

        Dim configfile As String = defaultConfigfile
        If args.Length > 0 Then
            configfile = args(0)
            raiseError("use argument0 " + configfile + "as config file, instead of default config file")
        End If

        deployMutex = New Mutex()
        config = New configMonitor()
        If config.open(configfile) Then
            raiseError("load configuration success, start service.")
            startService()
        Else
            raiseError("cannot load config file " + configfile, errorHandle.errorType.exclamation)
        End If
        destructor.stop()
        errorHandle.finalize()
    End Sub

    Private Sub config_ConfigChanged() Handles config.ConfigChanged
        raiseError("caught config file changed, start service again.")
        startDeploy()
    End Sub

    Private Sub functionlist_ConfigChanged() Handles funcConfig.ConfigChanged
        raiseError("caught functionlist file changed, start deploying.")
        startDeploy()
    End Sub

    Private Sub tcpServer_receive(ByVal handle As tcpServer.talkHandle) Handles tcpServer.receive
        If Not handle Is Nothing AndAlso _
           Not handle.receiveData Is Nothing AndAlso _
           handle.receiveData.fieldCount() > 0 Then
            Dim senddata As nettransData = Nothing
            senddata = New nettransData()

            Dim cmd As String = Nothing
            cmd = handle.receiveData.take(Of String)(0)
            If isemptystring(cmd) Then
                raiseError("cmd is nothing or emptystring.", errorHandle.errorType.warning)
            Else
                cmd = cmd.ToLower()
            End If

            If isdebugmode() Then
                raiseError("receive command " + cmd + " from " + _
                           displayHostPort(handle.remoteAddress(), handle.remotePort()))
            End If

            Select Case cmd
                Case protocol.command.deploy
                    raiseError("receive deploy command, start deploying.")
                    startDeploy()
                    senddata.insert(protocol.reply.success)
                Case protocol.command.ver
                    senddata.insert(buildInfo2)
                Case Else
                    senddata.insert(protocol.reply.unknown)
            End Select

            If Not handle.return(senddata) Then
                raiseError("cannot reply data to client " + _
                           displayHostPort(handle.remoteAddress(), handle.remotePort()) + _
                           ", data = " + senddata.display(), errorHandle.errorType.system)
            End If
        Else
            raiseError("receive invalid tcp request, ignore.", errorHandle.errorType.system)
        End If
    End Sub
End Module
